package daybook.io;

import daybook.Resources;
import daybook.exception.InvalidException;
import daybook.io.formatter.Format;
import daybook.model.Account;
import daybook.model.Book;
import daybook.model.Commodity;
import org.immutables.value.Value;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.time.temporal.IsoFields;
import java.time.temporal.TemporalUnit;
import java.util.Optional;
import java.util.Set;

@Value.Immutable
public abstract class GenerationConfig {
    private static final Logger LOG = LoggerFactory.getLogger(GenerationConfig.class);
    private static final Format DEFAULT_FORMAT = Format.STRING;

    @Value.Immutable
    public static abstract class LedgerConfig {
        public abstract Optional<Set<Account>> accounts();
        public abstract Format format();
    }

    @Value.Immutable
    public static abstract class JournalConfig {
        public abstract Optional<Set<Account>> accounts();
        public abstract LocalDate startDate();
        public abstract LocalDate endDate();
        public abstract Format format();
    }

    @Value.Immutable
    public static abstract class BalanceSheetConfig {
        public abstract LocalDate startDate();
        public abstract LocalDate endDate();
        public abstract TemporalUnit period();
        public abstract boolean ignoreZeroBalances();
        public abstract Format format();
    }

    @Value.Immutable
    public static abstract class IncomeStatementConfig {
        public abstract LocalDate startDate();
        public abstract LocalDate endDate();
        public abstract TemporalUnit period();
        public abstract boolean ignoreZeroBalances();
        public abstract Format format();
    }

    @Value.Immutable
    public static abstract class CashFlowConfig {
        public abstract Set<Account> accounts();
        public abstract LocalDate startDate();
        public abstract LocalDate endDate();
        public abstract TemporalUnit period();
        public abstract Format format();
    }

    @Value.Immutable
    public static abstract class InventoryConfig {
        public abstract Optional<Set<Commodity>> commodities();
        public abstract Format format();
    }

    public abstract Optional<LedgerConfig> ledger();
    public abstract Optional<JournalConfig> journal();
    public abstract Optional<BalanceSheetConfig> balanceSheet();
    public abstract Optional<IncomeStatementConfig> incomeStatement();
    public abstract Optional<CashFlowConfig> cashFlow();
    public abstract Optional<InventoryConfig> inventory();

    private static TemporalUnit parsePeriod(String period) {
        if (period.equals(IsoFields.QUARTER_YEARS.toString())) {
            return IsoFields.QUARTER_YEARS;
        } else {
            try {
                final TemporalUnit unit = ChronoUnit.valueOf(period.toUpperCase());
                if (!unit.isDateBased() || unit.getDuration().compareTo(ChronoUnit.YEARS.getDuration()) > 0) {
                    throw InvalidException.of(Resources.ErrorMessages.INVALID_TEMPORAL_UNIT, unit);
                }
                return unit;
            } catch (IllegalArgumentException e) {
                throw InvalidException.of(e, Resources.ErrorMessages.INVALID_TEMPORAL_UNIT, period);
            }
        }
    }

    public static GenerationConfig of(JSONObject json, Book book) {
        LOG.trace(">>> GenerationConfig.of");
        TemporalUnit defaultPeriod = ChronoUnit.MONTHS;
        LocalDate defaultStartDate = LocalDate.MIN;
        LocalDate defaultEndDate = LocalDate.MAX;
        Format defaultFormat = DEFAULT_FORMAT;
        boolean defaultIgnoreZeroBalances = true;
        if (!json.isNull("default")) {
            final JSONObject defaultJson = json.getJSONObject("default");
            if (!defaultJson.isNull("period")) {
                defaultPeriod = parsePeriod(defaultJson.getString("period"));
                LOG.debug("=== of: find default period: {}", defaultPeriod);
            }
            if (!defaultJson.isNull("startDate")) {
                defaultStartDate = LocalDate.parse(defaultJson.getString("startDate"));
                LOG.debug("=== of: find default start date: {}", defaultStartDate);
            }
            if (!defaultJson.isNull("endDate")) {
                defaultEndDate = LocalDate.parse(defaultJson.getString("endDate"));
                LOG.debug("=== of: find default end date: {}", defaultEndDate);
            }
            if (!defaultJson.isNull("format")) {
                defaultFormat = Format.valueOf(defaultJson.getString("format").toUpperCase());
                LOG.debug("=== of: find default format: {}", defaultFormat);
            }
            if (!defaultJson.isNull("ignoreZeroBalances")) {
                defaultIgnoreZeroBalances = defaultJson.getBoolean("ignoreZeroBalances");
                LOG.debug("=== of: find default ignoreZeroBalances: {}", defaultIgnoreZeroBalances);
            }
        }

        final ImmutableGenerationConfig.Builder builder = ImmutableGenerationConfig.builder();
        if (!json.isNull("ledger")) {
            final JSONObject ledgerJson = json.getJSONObject("ledger");
            LOG.debug("=== of: find ledger");
            final Set<Account> accounts = SimpleExpression.accountExpressionToSet(ledgerJson.opt("accounts"), book.getChartOfAccounts());
            if (LOG.isDebugEnabled()) {
                LOG.debug("=== of: ledger accounts: {}", SimpleExpression.accountsToString(accounts));
            }
            final Format format = ledgerJson.isNull("format") ? defaultFormat : Format.valueOf(ledgerJson.getString("format").toUpperCase());
            LOG.debug("=== of: ledger format: {}", format);
            final LedgerConfig ledgerConfig = ImmutableLedgerConfig.builder().accounts(Optional.ofNullable(accounts)).format(format).build();

            builder.ledger(ledgerConfig);
        }

        if (!json.isNull("journal")) {
            final JSONObject journalJson = json.getJSONObject("journal");
            LOG.debug("=== of: find journal");
            final Set<Account> accounts = SimpleExpression.accountExpressionToSet(journalJson.opt("accounts"), book.getChartOfAccounts());
            if (LOG.isDebugEnabled()) {
                LOG.debug("=== of: journal accounts: {}", SimpleExpression.accountsToString(accounts));
            }
            final Format format = journalJson.isNull("format") ? defaultFormat : Format.valueOf(journalJson.getString("format").toUpperCase());
            LOG.debug("=== of: journal format: {}", format);
            final LocalDate startDate = journalJson.isNull("startDate") ? defaultStartDate : LocalDate.parse(journalJson.getString("startDate"));
            LOG.debug("=== of: journal start date: {}", startDate);
            final LocalDate endDate = journalJson.isNull("endDate") ? defaultEndDate : LocalDate.parse(journalJson.getString("endDate"));
            LOG.debug("=== of: journal end date: {}", endDate);
            final JournalConfig journalConfig = ImmutableJournalConfig.builder()
                    .accounts(Optional.ofNullable(accounts)).startDate(startDate).endDate(endDate).format(format).build();
            builder.journal(journalConfig);
        }

        if (!json.isNull("balanceSheet")) {
            final JSONObject balanceSheetJson = json.getJSONObject("balanceSheet");
            LOG.debug("=== of: find balanceSheet");
            final Format format = balanceSheetJson.isNull("format") ? defaultFormat : Format.valueOf(balanceSheetJson.getString("format").toUpperCase());
            LOG.debug("=== of: balanceSheet format: {}", format);
            final LocalDate startDate = balanceSheetJson.isNull("startDate") ? defaultStartDate : LocalDate.parse(balanceSheetJson.getString("startDate"));
            LOG.debug("=== of: balanceSheet start date: {}", startDate);
            final LocalDate endDate = balanceSheetJson.isNull("endDate") ? defaultEndDate : LocalDate.parse(balanceSheetJson.getString("endDate"));
            LOG.debug("=== of: balanceSheet end date: {}", endDate);
            final TemporalUnit period = balanceSheetJson.isNull("period") ? defaultPeriod : parsePeriod(balanceSheetJson.getString("period"));
            LOG.debug("=== of: balanceSheet period: {}", period);
            final boolean ignoreZeroBalances = balanceSheetJson.optBoolean("ignoreZeroBalances", defaultIgnoreZeroBalances);
            LOG.debug("=== of: balanceSheet ignoreZeroBalances: {}", ignoreZeroBalances);
            final BalanceSheetConfig balanceSheetConfig = ImmutableBalanceSheetConfig.builder()
                    .startDate(startDate).endDate(endDate).period(period).format(format).ignoreZeroBalances(ignoreZeroBalances).build();
            builder.balanceSheet(balanceSheetConfig);
        }

        if (!json.isNull("incomeStatement")) {
            final JSONObject incomeStatementJson = json.getJSONObject("incomeStatement");
            LOG.debug("=== of: find incomeStatement");
            final Format format = incomeStatementJson.isNull("format") ? defaultFormat : Format.valueOf(incomeStatementJson.getString("format").toUpperCase());
            LOG.debug("=== of: incomeStatement format: {}", format);
            final LocalDate startDate = incomeStatementJson.isNull("startDate") ? defaultStartDate : LocalDate.parse(incomeStatementJson.getString("startDate"));
            LOG.debug("=== of: incomeStatement start date: {}", startDate);
            final LocalDate endDate = incomeStatementJson.isNull("endDate") ? defaultEndDate : LocalDate.parse(incomeStatementJson.getString("endDate"));
            LOG.debug("=== of: incomeStatement end date: {}", endDate);
            final TemporalUnit period = incomeStatementJson.isNull("period") ? defaultPeriod : parsePeriod(incomeStatementJson.getString("period"));
            LOG.debug("=== of: incomeStatement period: {}", period);
            final boolean ignoreZeroBalances = incomeStatementJson.optBoolean("ignoreZeroBalances", defaultIgnoreZeroBalances);
            LOG.debug("=== of: incomeStatement ignoreZeroBalances: {}", ignoreZeroBalances);
            final IncomeStatementConfig incomeStatementConfig = ImmutableIncomeStatementConfig.builder()
                    .startDate(startDate).endDate(endDate).period(period).format(format).ignoreZeroBalances(ignoreZeroBalances).build();
            builder.incomeStatement(incomeStatementConfig);
        }

        if (!json.isNull("cashFlow")) {
            final JSONObject cashFlowJson = json.getJSONObject("cashFlow");
            LOG.debug("=== of: find cashFlow");
            final Set<Account> accounts = SimpleExpression.accountExpressionToSet(cashFlowJson.get("accounts"), book.getChartOfAccounts());
            if (accounts == null || accounts.isEmpty()) {
                LOG.debug("=== of: cashFlow not specify cash accounts or cash accounts are empty");
                throw InvalidException.of(Resources.ErrorMessages.INVALID_CASH_FLOW_ACCOUNTS);
            }
            if (LOG.isDebugEnabled()) {
                LOG.debug("=== of: cashFlow accounts: {}", SimpleExpression.accountsToString(accounts));
            }
            final Format format = cashFlowJson.isNull("format") ? defaultFormat : Format.valueOf(cashFlowJson.getString("format").toUpperCase());
            LOG.debug("=== of: cashFlow format: {}", format);
            final LocalDate startDate = cashFlowJson.isNull("startDate") ? defaultStartDate : LocalDate.parse(cashFlowJson.getString("startDate"));
            LOG.debug("=== of: cashFlow start date: {}", startDate);
            final LocalDate endDate = cashFlowJson.isNull("endDate") ? defaultEndDate : LocalDate.parse(cashFlowJson.getString("endDate"));
            LOG.debug("=== of: cashFlow end date: {}", endDate);
            final TemporalUnit period = cashFlowJson.isNull("period") ? defaultPeriod : parsePeriod(cashFlowJson.getString("period"));
            LOG.debug("=== of: cashFlow period: {}", period);
            final CashFlowConfig cashFlowConfig = ImmutableCashFlowConfig.builder()
                    .accounts(accounts).startDate(startDate).endDate(endDate).period(period).format(format).build();
            builder.cashFlow(cashFlowConfig);
        }

        if (!json.isNull("inventory")) {
            final JSONObject inventoryJson = json.getJSONObject("inventory");
            LOG.debug("=== of: find inventory");
            final Set<Commodity> commodities = SimpleExpression.commodityExpressionToSet(inventoryJson.opt("commodities"), book.getCommodities());
            if (LOG.isDebugEnabled()) {
                LOG.debug("=== of: inventory commodities: {}", SimpleExpression.commoditiesToString(commodities));
            }
            final Format format = inventoryJson.isNull("format") ? defaultFormat : Format.valueOf(inventoryJson.getString("format").toUpperCase());
            LOG.debug("=== of: inventory format: {}", format);
            final InventoryConfig inventoryConfig = ImmutableInventoryConfig.builder()
                    .commodities(Optional.ofNullable(commodities)).format(format).build();
            builder.inventory(inventoryConfig);
        }

        LOG.trace("<<< GenerationConfig.of");
        return builder.build();
    }
}
